﻿/// <summary>
/// Code regarding keyboard and mouse input in this file comes from the TestApi project in CodePlex, http://testapi.codeplex.com/
/// </summary>

namespace InputHandlers.KeyboardHandling
{
    using System;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.Windows.Forms;
    using NativeCode;

    /// <summary>
    /// Synthesize keyboard press, release and type operations.
    /// </summary>
    public static class KeyboardSynth
    {
        private static readonly object s_typeSync = new object();

        /// <summary>
        /// Performs a press-and-release operation for the specified key, which is effectively equivallent to typing.
        /// </summary>
        /// <param name="key">The key to press.</param>
        /// <param name="sleepTime">Optionally sleep time between keyboard key strokes.</param>
        public static void Type(Keys key, TimeSpan? sleepTime = null)
        {
            Press(key);
            Release(key, sleepTime);
        }

        /// <summary>
        /// Type the specified input string to the current keyboard-focused control.
        /// </summary>
        /// <param name="input">The string to type.</param>
        /// <param name="sleepTime">Optionally sleep time between keyboard key strokes.</param>
        /// <param name="disableKeyboardHooks">Whether to disable keyboard hooks.</param>
        public static void Type(string input, TimeSpan? sleepTime = null, bool disableKeyboardHooks = true)
        {
            lock (s_typeSync)
            {
                if (disableKeyboardHooks)
                {
                    KeyboardGlobalInterceptor.Enabled = false;
                }

                foreach (var ch in input)
                {
                    // If code point is bigger than 8 bits, we are going for Unicode approach by setting wVk to 0.
                    if (ch > 0xFE)
                    {
                        SendKeyboardKey(ch, true, false, true);
                        SendKeyboardKey(ch, false, false, true, sleepTime);
                    }
                    else
                    {
                        // We get the vKey value for the character via a Win32 API. We then use bit masks to pull the
                        // upper and lower bytes to get the shift state and key information. We then use WPF KeyInterop
                        // to go from the vKey key info into a System.Windows.Input.Key data structure. This work is
                        // necessary because Key doesn't distinguish between upper and lower case, so we have to wrap
                        // the key type inside a shift press/release if necessary.
                        int vKeyValue = NativeMethods.VkKeyScan(ch);
                        bool keyIsShifted = (vKeyValue & NativeMethods.VKeyShiftMask) == NativeMethods.VKeyShiftMask;
                        var key = (Keys)(vKeyValue & NativeMethods.VKeyCharMask);

                        if (keyIsShifted)
                        {
                            Type(key, new[] { Keys.LShiftKey }, sleepTime);
                        }
                        else
                        {
                            Type(key, sleepTime);
                        }
                    }
                }

                if (disableKeyboardHooks)
                {
                    KeyboardGlobalInterceptor.Enabled = true;
                }
            }
        }

        /// <summary>
        /// Presses down a key.
        /// </summary>
        /// <param name="key">The key to press.</param>
        /// <param name="sleepTime">Optionally sleep time between keyboard key strokes.</param>
        public static void Press(Keys key, TimeSpan? sleepTime = null)
        {
            SendKeyboardKey((ushort)key, true, ToggleKeyFactory.IsExtendedKey(key), false, sleepTime);
        }

        /// <summary>
        /// Releases a key.
        /// </summary>
        /// <param name="key">The key to release.</param>
        /// <param name="sleepTime">Optionally sleep time between keyboard key strokes.</param>
        public static void Release(Keys key, TimeSpan? sleepTime = null)
        {
            SendKeyboardKey((ushort)key, false, ToggleKeyFactory.IsExtendedKey(key), false, sleepTime);
        }

        #region Helper methods

        private static void Type(Keys key, Keys[] modifierKeys, TimeSpan? sleepTime = null)
        {
            foreach (var modifierKey in modifierKeys)
            {
                Press(modifierKey);
            }

            Type(key, sleepTime);

            foreach (var modifierKey in modifierKeys.Reverse())
            {
                Release(modifierKey);
            }
        }

        private static void SendKeyboardKey(ushort key, bool isKeyDown, bool isExtended, bool isUnicode, TimeSpan? sleepTime = null)
        {
            var input = new NativeMethods.INPUT();
            input.Type = NativeMethods.INPUT_KEYBOARD;
            if (!isKeyDown)
            {
                input.Data.Keyboard.dwFlags |= NativeMethods.KEYEVENTF_KEYUP;
            }

            if (isUnicode)
            {
                input.Data.Keyboard.dwFlags |= NativeMethods.KEYEVENTF_UNICODE;
                input.Data.Keyboard.wScan = key;
                input.Data.Keyboard.wVk = 0;
            }
            else
            {
                input.Data.Keyboard.wScan = 0;
                input.Data.Keyboard.wVk = key;
            }

            if (isExtended)
            {
                input.Data.Keyboard.dwFlags |= NativeMethods.KEYEVENTF_EXTENDEDKEY;
            }

            input.Data.Keyboard.time = 0;
            input.Data.Keyboard.dwExtraInfo = IntPtr.Zero;

            NativeMethods.SendInput(1, new[] { input }, Marshal.SizeOf(input));

            sleepTime = sleepTime ?? TimeSpan.Zero;
            Thread.Sleep(sleepTime.Value);
        }

        #endregion
    }
}

